"""
editor.py - http://infomesh.net/2002/editor/
A Simple GUI Text Editing Thingy, 2002-01

by Sean B. Palmer, http://purl.org/net/sbp/

This is based on PyEdit 1.1, by Mark Lutz 
(http://rmi.net/~lutz)

Modified by Rob Speer for the 6.863 Earley Parser.
"""

import sys, os, re, urllib, Tkinter
import tkFileDialog, tkMessageBox, tkSimpleDialog

__version__ = '1.1.2'
__license__ = 'GNU General Public License 2'

USER = os.environ['USER']

#NAME, URI = 'editor.py', 'http://infomesh.net/2002/editor/'
NAME, URI = '6.863 Parser', ''
SEL_FIRST, SEL_LAST = Tkinter.SEL+'.first', Tkinter.SEL+'.last'

if sys.platform[:3].lower() != 'win': FontScale = 3
else: FontScale = 0

class AppURLopener(urllib.FancyURLopener):
    def __init__(self, *args):
        self.version = '%s/%s (%s)' % (NAME, __version__, URI)
        apply(urllib.FancyURLopener.__init__, (self,) + args)
urllib._urlopener = AppURLopener()

class TextEditor(Tkinter.Frame): 
    startfiledir = '.'
    ftypes = [('Grammar files',   '.grm'), 
              ('Text files',  '.txt .text'), 
              ('All files',   '*')]
    color, font = ['black', 'white'], ('andale mono', 8+FontScale, 'normal')

    def __init__(self, parent=None, loadFirst=''):
        Tkinter.Frame.__init__(self)
        self.pack(expand=Tkinter.YES, fill=Tkinter.BOTH)
        self.start()
        self.makeMenuBar()
        self.makeWidgets()
        self.setFileName([None, None])
        self.lastfind = None
        self.openDialog = None
        self.saveDialog = None
        self.text.focus()
        if loadFirst: self.open(loadFirst)
        self.master.title(NAME+' '+__version__)
        self.master.iconname(NAME)
        self.master.protocol('WM_DELETE_WINDOW', self.quit)
        self.panicmode = False
        self.setHash()

    def setHash(self):
        self.hash = hash(self.getAllText())
    
    def start(self): 
        self.menuBar = [
            ('File', 0, 
                 [('New',         0, self.new, 'N'), 
                  ('Open...',     0, self.open, 'O'), 
                  ('Save',        0, self.save, 'S'), 
                  ('Save As...',  5, self.saveAs), 
                  'separator',
                  ('Quit...',     0, self.quit, 'Q')]), 
            ('Edit', 0,
                 [('Cut',         0, self.cut, 'X'), 
                  ('Copy',        1, self.copy, 'C'), 
                  ('Paste',       0, self.paste, 'V'), 
                  'separator',
                  ('Delete',      0, self.delete), 
                  ('Select All',  0, self.selectAll, 'A')]),
            ('Search', 0,
                 [('Find...',        0, self.findPrompt, 'F'), 
                  ('Find next',      1, self.findAgain, 'G'), 
                  ('Replace...',     0, self.replace), 
                  'separator', 
                  ('Goto line...',   0, self.gotoLine, 'L')]),
            ('Recover', 0,
                 [('Recover unsaved file', 0, self.recover)])
#            ('Help', 0,
#                 [('About '+NAME,    0, self.help), 
#                  ('Documentation',  0, self.documentation)])
           ]
    
    def quit(self):
        if not self.isEmpty(): self.beNice()
        self.master.destroy()
    
    def beNice(self):
        self.saveAs('/tmp/earley_recover_%s.txt' % USER)
  
    def addMenuItems(self, menu, items):
        import types
        for item in items: 
            if item == 'separator': menu.add_separator({})
            elif type(item) == types.ListType: 
                for num in item: menu.entryconfig(num, state=Tkinter.DISABLED)
            elif type(item[2]) != types.ListType:
                if len(item) > 3:
                    menu.add_command(label = item[0], underline = item[1], \
                       command = item[2], accelerator = "Ctrl+"+item[3])
                else:
                    menu.add_command(label = item[0], underline = item[1], \
                       command = item[2])
            else:
                p = Tkinter.Menu(menu) # pullover
                self.addMenuItems(p, item[2])
                menu.add_cascade(label = item[0], underline = item[1], menu = p)
        self.master.bind('<Control-n>', lambda ignore: self.new())
        self.master.bind('<Control-o>', lambda ignore: self.open(confirm=False))
        self.master.bind('<Control-s>', lambda ignore: self.save())
        self.master.bind('<Control-q>', lambda ignore: self.quit())
        self.master.bind('<Control-x>', lambda ignore: self.cut())
        self.master.bind('<Control-c>', lambda ignore: self.copy())
        self.master.bind('<Control-v>', lambda ignore: self.paste())
        self.master.bind('<Control-a>', lambda ignore: self.selectAll())
        self.master.bind('<Control-f>', lambda ignore: self.findPrompt())
        self.master.bind('<Control-g>', lambda ignore: self.findAgain())
        self.master.bind('<Control-l>', lambda ignore: self.gotoLine())

    def makeWidgets(self): 
        vbar = Tkinter.Scrollbar(self)  
        hbar = Tkinter.Scrollbar(self, orient='horizontal')
        text = Tkinter.Text(self, padx=5, wrap='none') 

        vbar.pack(side=Tkinter.RIGHT, fill=Tkinter.Y)
        hbar.pack(side=Tkinter.BOTTOM, fill=Tkinter.X)
        text.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=Tkinter.YES)

        text.config(yscrollcommand=vbar.set)
        text.config(xscrollcommand=hbar.set)

        vbar.config(command=text.yview)
        hbar.config(command=text.xview)

        text.config(font=self.font, bg=self.color[1], fg=self.color[0])
        self.text = text

    def makeMenuBar(self):
        menubar = Tkinter.Menu(self.master)
        self.master.config(menu=menubar)

        for menuitem in self.menuBar:
            (name, key, items) = menuitem[:3]
            pulldown = Tkinter.Menu(menubar)
            self.addMenuItems(pulldown, items)
            menubar.add_cascade(label=name, underline=key, menu=pulldown)

    # File menu commands

    def new(self):
        if not self.isEmpty(): self.beNice()
        self.setFileName([None, None])
        self.clearAllText()
        self.setHash()

    def my_askopenfilename(self): 
        if not self.openDialog:
           self.openDialog = tkFileDialog.Open(initialdir=self.startfiledir, 
                                  filetypes=self.ftypes)
        return self.openDialog.show()

    def open(self, loadFirst=''):
        if not loadFirst and not self.isEmpty(): self.beNice()
        doit = True
        if doit:
            file = loadFirst or self.my_askopenfilename()
            if file:
                try: text = open(file, 'r').read()
                except: tkMessageBox.showerror(NAME, 'Could not open file '+file)
                else:
                   self.setAllText(text)
                   self.setFileName([file, 'File'])
                   self.setHash()

    def openFromWeb(self, loadFirst=''):
        doit = True
        if doit:
            uri = loadFirst or tkSimpleDialog.askstring(NAME, 'Enter URI:')
            if uri: 
                try: 
                   text = urllib.urlopen(uri).read()
                   text = text.replace('\r\n', '\n').replace('\r', '\n')
                except: tkMessageBox.showerror(NAME, 'Could not open URI '+uri)
                else:
                   self.setAllText(text)
                   self.setFileName([uri, 'URI'])

    def recover(self):
        self.open('/tmp/earley_recover_%s.txt' % USER)

    def my_asksaveasfilename(self): 
        if not self.saveDialog:
           self.saveDialog = tkFileDialog.SaveAs(initialdir=self.startfiledir, 
                                    filetypes=self.ftypes)
        return self.saveDialog.show()

    def save(self): 
        if self.current[1] != 'URI': self.saveAs(self.current[0])
        else: self.saveToWeb(self.current[0])

    def saveAs(self, forcefile=None): 
        file = forcefile or self.my_asksaveasfilename()
        if file:
            text = self.getAllText()
            try: open(file, 'w').write(text)
            except: 
               tkMessageBox.showerror(NAME, 'Could not write file '+str(file))
            else:
                self.setFileName([file, 'File'])
                self.setHash()

    def saveToWeb(self, forcefile=None): 
        uri = forcefile or tkSimpleDialog.askstring(NAME, 'Enter URI:')
        if uri: 
            try: 
               import savetoweb
               text = self.getAllText()
               try: 
                  savetoweb.uploadData(text, savetoweb.doMappings(uri))
                  tkMessageBox.showinfo(NAME, 'Saved successfully to '+uri)
               except: 
                  tkMessageBox.showerror(NAME, 'Could not write file '+str(uri))
               else: self.setFileName([uri, 'URI'])
            except: tkMessageBox.showerror(NAME, 'Can\'t save to Web')

    # Edit menu commands

    def cut(self, *args):
        if not self.text.tag_ranges(Tkinter.SEL):
            return
        else: 
            self.copy()
            self.delete()

    def copy(self, *args): 
        if not self.text.tag_ranges(Tkinter.SEL): 
            return
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)  
            self.clipboard_clear()              
            self.clipboard_append(text)

    def paste(self, *args):
        try: text = self.selection_get(selection='CLIPBOARD')
        except TclError:
            return
        self.text.insert(Tkinter.INSERT, text)
        self.text.tag_remove(Tkinter.SEL, '1.0', Tkinter.END) 
        self.text.tag_add(Tkinter.SEL, Tkinter.INSERT+'-%dc' % len(text), \
           Tkinter.INSERT)
        self.text.see(Tkinter.INSERT)

    def delete(self, *args): 
        if not self.text.tag_ranges(Tkinter.SEL):
            tkMessageBox.showerror(NAME, 'No text selected')
        else: self.text.delete(SEL_FIRST, SEL_LAST)

    def selectAll(self, *args):
        self.text.tag_add(Tkinter.SEL, '1.0', Tkinter.END+'-1c')
        self.text.mark_set(Tkinter.INSERT, '1.0')
        self.text.see(Tkinter.INSERT)

    # Tools menu commands 

    def docInfo(self, *args):
        text  = self.getAllText()
        bytes = len(text), 
        lines, words = len(text.split('\n')), len(text.split())
        index = self.text.index(Tkinter.INSERT)
        where = tuple(index.split('.'))
        m = 'Current location:\n File:\t%s\n Line:\t%s \n Col:\t%s\n\n' % \
               (str(self.current[0]), where[0], where[1])
        m += 'File text statistics:\n Bytes:\t%s\n Lines:\t%s\n Words:\t%s' % \
                (bytes[0], lines, words)
        new = Tkinter.Toplevel()
        text = Tkinter.Text(new, padx=5, wrap='none')
        text.master.title(NAME+' Information')
        text.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=Tkinter.YES)
        text.config(font=self.font, bg='white', fg='black')
        text.insert(Tkinter.INSERT, m)
        text.focus()

    # Search menu commands
 
    def gotoLine(self):
        line = tkSimpleDialog.askinteger(NAME, 'Enter line number')
        self.text.update() 
        self.text.focus()
        if line is not None:
            maxindex = self.text.index(Tkinter.END+'-1c')
            maxline  = int(maxindex.split('.')[0])
            if (line > 0) and (line <= maxline):
                self.text.mark_set(Tkinter.INSERT, '%d.0' % line)
                self.text.tag_remove(Tkinter.SEL, '1.0', Tkinter.END)
                self.text.tag_add(Tkinter.SEL, Tkinter.INSERT, 'insert + 1l')
                self.text.see(Tkinter.INSERT)
            else: tkMessageBox.showerror(NAME, 'Bad line number')

    def findPrompt(self):
        new = Tkinter.Toplevel(self)
        new.title(NAME+' Find')
        Tkinter.Label(new, text='Find text:').grid(row=0, column=0)
        self.change1 = Tkinter.Entry(new)
        self.change1.grid(row=0, column=1, sticky=Tkinter.EW)
        Tkinter.Button(new, text='Find', default=Tkinter.ACTIVE, 
               command=self.onDoFind).grid(row=0, column=2, sticky=Tkinter.EW)
        Tkinter.Button(new, text='Close',  
               command=new.destroy).grid(row=1, column=2, sticky=Tkinter.EW)
        new.bind("<Return>", self.onDoFind)
        new.columnconfigure(1, weight=1)
        self.change1.focus()

    def find(self, lastkey=None, bust=None):
        key = lastkey
        self.text.update()
        self.text.focus()
        self.lastfind = key
        if key:
            where = self.text.search(key, Tkinter.INSERT, Tkinter.END)
            if not where: 
               if bust: raise 'bust'
               else: tkMessageBox.showerror(NAME, 'String not found')
            else:
                pastkey = where + '+%dc' % len(key)
                self.text.tag_remove(Tkinter.SEL, '1.0', Tkinter.END)
                self.text.tag_add(Tkinter.SEL, where, pastkey)
                self.text.mark_set(Tkinter.INSERT, pastkey)
                self.text.see(where)

    def findAgain(self): 
        self.find(self.lastfind)

    def replace(self): 
        new = Tkinter.Toplevel(self)
        Tkinter.Label(new, text='Find text:').grid(row=0, column=0)
        Tkinter.Label(new, text='Change to:').grid(row=1, column=0)
        self.change1 = Tkinter.Entry(new)
        self.change2 = Tkinter.Entry(new)
        self.change1.grid(row=0, column=1, sticky=Tkinter.EW)
        self.change2.grid(row=1, column=1, sticky=Tkinter.EW)
        Tkinter.Button(new, text='Find Next',  
               command=self.onDoFind).grid(row=0, column=2, sticky=Tkinter.EW)
        Tkinter.Button(new, text='Replace', 
               command=self.doReplace).grid(row=1, column=2, sticky=Tkinter.EW)
        Tkinter.Button(new, text='Replace All', 
               command=self.replaceAll).grid(row=2, column=2, sticky=Tkinter.EW)
        new.columnconfigure(1, weight=1)
        new.focus()

    def onDoFind(self, event=None):
        self.find(self.change1.get())

    def doReplace(self, bust=None):
        self.find(self.change1.get(), bust=bust)
        self.text.update()
        if self.text.tag_ranges(Tkinter.SEL): 
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.text.insert(Tkinter.INSERT, self.change2.get())
            self.text.see(Tkinter.INSERT)

    def replaceAll(self): 
        try: 
            self.doReplace(1)
            self.replaceAll()
        except: pass

    # Others, useful outside this class

    def undo(self):
        self.text.edit_undo()

    def redo(self):
        self.text.edit_redo()
        
    def panic(self):
        tkMessageBox.showinfo("Uh-oh",
        """Good. At least you didn't lose your work.

However, due to a heinous bug in Tkinter, once you answer 'no' to a dialog, you can't ever answer 'yes' to that same dialog. Yes, this is very dumb.

All I can do is not bring up any more question dialogs. So either restart the program now, or be sure you know what you're doing!
        """)
        self.panicmode = True

    def isEmpty(self):
        # Not just 'empty', but 'unchanged' is what we're looking for.
        return self.hash == hash(self.getAllText())

    def getAllText(self):
        return self.text.get('1.0', Tkinter.END+'-1c')

    def setAllText(self, text):
        self.text.delete('1.0', Tkinter.END)
        self.text.insert(Tkinter.END, text)
        self.text.mark_set(Tkinter.INSERT, '1.0')
        self.text.see(Tkinter.INSERT)

    def clearAllText(self):
        self.text.delete('1.0', Tkinter.END)

    def getFileName(self):
        return self.current[0]

    def setFileName(self, name): 
        self.current = name
        self.master.title(NAME+' '+__version__+' - '+str(name[0]))

    def help(self):
        tkMessageBox.showinfo('About '+NAME, __doc__)

    def documentation(self): 
        self.openFromWeb(URI+'index.txt')

def main(): 
    if len(sys.argv) > 1: fname = sys.argv[1]
    else: fname = None
    TextEditor(loadFirst=fname).pack(expand=Tkinter.YES, fill=Tkinter.BOTH)
    Tkinter.mainloop()

if __name__ == '__main__': 
    main()

# vim:ts=4:expandtab:
